Skip to content

feat(core, node): portable Express integration#19928

Open
isaacs wants to merge 2 commits intodevelopfrom
isaacschlueter/portable-express-integration
Open

feat(core, node): portable Express integration#19928
isaacs wants to merge 2 commits intodevelopfrom
isaacschlueter/portable-express-integration

Conversation

@isaacs
Copy link
Copy Markdown
Member

@isaacs isaacs commented Mar 21, 2026

This extracts the functionality from the OTel Express intstrumentation,
replacing it with a portable standalone integration in @sentry/core,
which can be extended and applied to patch any Express module import in
whatever way makes sense for the platform in question.

Currently in node, that is still an OpenTelemetry intstrumentation, but
just handling the automatic module load instrumentation, not the entire
tracing integration.

This is somewhat a proof of concept, to see what it takes to port a
fairly invovled OTel integration into a state where it can support all
of the platforms that we care about, but it does impose a bit less of a
translation layer between OTel and Sentry semantics (for example, no
need to use the no-op span.recordException()).

User-visible changes (beyond the added export in @sentry/core):

  • Express router spans have an origin of auto.http.express rather than
    auto.http.otel.express, since it's no longer technically an otel
    integration.
  • The empty measurements: {} object is no longer attached to span
    data, as that was an artifact of otel's span.recordError, which is a
    no-op anyway, and no longer executed.

Obviously this is not a full clean-room reimplementation, and relies on
the fact that the opentelemetry-js-contrib project is Apache 2.0
licensed. I included the link to the upstream license in the index file
for the Express integration, but there may be a more appropriate way to
ensure that the license is respected properly. It was arguably a
derivative work already, but simple redistribution is somewhat different
than re-implementation with subtly different context.

This reduces the node overhead and makes the Express instrumentation
portable to other SDKs, but it of course increases the bundle size of
@sentry/core considerably. It would be a good idea to explore
splitting out integrations from core, so that they're bundled and
analyzed separately, rather than shipping to all SDKs that extend core.

Closes #19929 (added automatically)

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 21, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

Deps

  • Bump babel-loader from 10.0.0 to 10.1.1 by dependabot in #19997
  • Bump handlebars from 4.7.7 to 4.7.9 by dependabot in #20008

Nuxt

  • Add middleware instrumentation compatibility for Nuxt 5 by s1gr1d in #19968
  • Support parametrized SSR routes in Nuxt 5 by s1gr1d in #19977

Other

  • (browser) Replace element timing spans with metrics by logaretm in #19869
  • (bun) Add bunRuntimeMetricsIntegration by chargome in #19979
  • (core) Support embedding APIs in google-genai by nicohrubec in #19797
  • (core, node) Portable Express integration by isaacs in #19928
  • (node) Add nodeRuntimeMetricsIntegration by chargome in #19923

Bug Fixes 🐛

  • (e2e) Pin @opentelemetry/api to 1.9.0 in ts3.8 test app by logaretm in #19992
  • (node) Ensure startNewTrace propagates traceId in OTel environments by logaretm in #19963
  • (nuxt) Use virtual module for Nuxt pages data (SSR route parametrization) by s1gr1d in #20020
  • (opentelemetry) Convert seconds timestamps in span.end() to milliseconds by logaretm in #19958

Documentation 📚

  • (release) Update publishing-a-release.md by nicohrubec in #19982

Internal Changes 🔧

Core

  • Introduce instrumented method registry for AI integrations by nicohrubec in #19981
  • Consolidate getOperationName into one shared utility by nicohrubec in #19971

Deps

  • Bump amqplib from 0.10.7 to 0.10.9 by dependabot in #20000
  • Bump actions/upload-artifact from 6 to 7 by dependabot in #19569
  • Bump srvx from 0.11.12 to 0.11.13 by dependabot in #20001
  • Bump @apollo/server from 5.4.0 to 5.5.0 by dependabot in #20007

Deps Dev

  • Remove esbuild override in astro-5-cf-workers E2E test by isaacs in #20024
  • Bump node-forge from 1.3.2 to 1.4.0 by dependabot in #20012
  • Bump yaml from 2.8.2 to 2.8.3 by dependabot in #19985

Other

  • (deno) Expand Deno E2E test coverage by chargome in #19957
  • (e2e) Add e2e tests for nodeRuntimeMetricsIntegration by chargome in #19989

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 21, 2026

size-limit report 📦

⚠️ Warning: Base artifact is not the latest one, because the latest workflow run is not done yet. This may lead to incorrect results. Try to re-run all tests to get up to date results.

Path Size % Change Change
@sentry/browser 25.69 kB +0.02% +5 B 🔺
@sentry/browser - with treeshaking flags 24.18 kB +0.03% +5 B 🔺
@sentry/browser (incl. Tracing) 42.18 kB +0.02% +7 B 🔺
@sentry/browser (incl. Tracing, Profiling) 46.8 kB +0.02% +6 B 🔺
@sentry/browser (incl. Tracing, Replay) 80.99 kB +0.01% +6 B 🔺
@sentry/browser (incl. Tracing, Replay) - with treeshaking flags 70.6 kB +0.01% +5 B 🔺
@sentry/browser (incl. Tracing, Replay with Canvas) 85.71 kB +0.02% +9 B 🔺
@sentry/browser (incl. Tracing, Replay, Feedback) 97.97 kB +0.01% +6 B 🔺
@sentry/browser (incl. Feedback) 42.48 kB +0.02% +6 B 🔺
@sentry/browser (incl. sendFeedback) 30.36 kB +0.02% +6 B 🔺
@sentry/browser (incl. FeedbackAsync) 35.41 kB +0.02% +7 B 🔺
@sentry/browser (incl. Metrics) 26.96 kB +0.03% +7 B 🔺
@sentry/browser (incl. Logs) 27.1 kB +0.03% +6 B 🔺
@sentry/browser (incl. Metrics & Logs) 27.79 kB +0.03% +7 B 🔺
@sentry/react 27.45 kB +0.03% +6 B 🔺
@sentry/react (incl. Tracing) 44.53 kB +0.02% +6 B 🔺
@sentry/vue 30.14 kB +0.02% +6 B 🔺
@sentry/vue (incl. Tracing) 44.08 kB +0.02% +6 B 🔺
@sentry/svelte 25.71 kB +0.02% +5 B 🔺
CDN Bundle 28.4 kB +0.02% +5 B 🔺
CDN Bundle (incl. Tracing) 43.2 kB +0.02% +6 B 🔺
CDN Bundle (incl. Logs, Metrics) 29.77 kB +0.02% +4 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) 44.26 kB +0.02% +6 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) 68.56 kB +0.01% +6 B 🔺
CDN Bundle (incl. Tracing, Replay) 80.09 kB +0.01% +7 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) 81.16 kB +0.01% +5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) 85.62 kB +0.01% +5 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) 86.68 kB +0.01% +5 B 🔺
CDN Bundle - uncompressed 82.95 kB +0.03% +22 B 🔺
CDN Bundle (incl. Tracing) - uncompressed 128.09 kB +0.02% +22 B 🔺
CDN Bundle (incl. Logs, Metrics) - uncompressed 87.09 kB +0.03% +22 B 🔺
CDN Bundle (incl. Tracing, Logs, Metrics) - uncompressed 131.5 kB +0.02% +22 B 🔺
CDN Bundle (incl. Replay, Logs, Metrics) - uncompressed 210.08 kB +0.02% +22 B 🔺
CDN Bundle (incl. Tracing, Replay) - uncompressed 244.97 kB +0.01% +22 B 🔺
CDN Bundle (incl. Tracing, Replay, Logs, Metrics) - uncompressed 248.37 kB +0.01% +22 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback) - uncompressed 257.88 kB +0.01% +22 B 🔺
CDN Bundle (incl. Tracing, Replay, Feedback, Logs, Metrics) - uncompressed 261.27 kB +0.01% +22 B 🔺
@sentry/nextjs (client) 46.94 kB +0.02% +7 B 🔺
@sentry/sveltekit (client) 42.68 kB +0.02% +7 B 🔺
@sentry/node-core 56.52 kB +0.05% +23 B 🔺
@sentry/node 173.29 kB -0.18% -304 B 🔽
@sentry/node - without tracing 96.56 kB +0.03% +27 B 🔺
@sentry/aws-serverless 113.56 kB +0.03% +32 B 🔺

View base workflow run

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 21, 2026

node-overhead report 🧳

Note: This is a synthetic benchmark with a minimal express app and does not necessarily reflect the real-world performance impact in an application.

Scenario Requests/s % of Baseline Prev. Requests/s Change %
GET Baseline 8,987 - 8,837 +2%
GET With Sentry 1,727 19% 1,654 +4%
GET With Sentry (error only) 5,942 66% 6,073 -2%
POST Baseline 1,185 - 1,202 -1%
POST With Sentry 588 50% 591 -1%
POST With Sentry (error only) 1,046 88% 1,051 -0%
MYSQL Baseline 3,205 - 3,229 -1%
MYSQL With Sentry 410 13% 487 -16%
MYSQL With Sentry (error only) 2,603 81% 2,640 -1%

View base workflow run

@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from 3cddcee to dc1b9cf Compare March 21, 2026 23:48
@isaacs isaacs marked this pull request as draft March 21, 2026 23:48
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch 5 times, most recently from 64c45e7 to fd4fefe Compare March 22, 2026 19:04
@isaacs isaacs marked this pull request as ready for review March 22, 2026 21:03
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from 7e0d74f to db667fc Compare March 23, 2026 03:45
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch 2 times, most recently from b2d4e3c to d5d4e9b Compare March 23, 2026 14:07
@linear-code linear-code bot mentioned this pull request Mar 23, 2026
24 tasks
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch 2 times, most recently from f694a6b to 23f24a1 Compare March 23, 2026 18:10
@isaacs isaacs requested a review from Lms24 March 23, 2026 19:00
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from 23f24a1 to 37d4883 Compare March 24, 2026 23:53
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Autofix Details

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Patched layer.handle becomes non-writable unintentionally
    • Added writable: true to the Object.defineProperty descriptor so patched layer.handle preserves Express's original writability for direct reassignment.

Create PR

Or push these changes by commenting:

@cursor push 6524f61f18
Preview (6524f61f18)
diff --git a/packages/core/src/integrations/express/patch-layer.ts b/packages/core/src/integrations/express/patch-layer.ts
--- a/packages/core/src/integrations/express/patch-layer.ts
+++ b/packages/core/src/integrations/express/patch-layer.ts
@@ -46,6 +46,7 @@
   Object.defineProperty(layer, 'handle', {
     enumerable: true,
     configurable: true,
+    writable: true,
     value: function layerHandlePatched(
       this: ExpressLayer,
       req: ExpressRequest,

This Bugbot Autofix run was free. To enable autofix for future PRs, go to the Cursor dashboard.

Copy link
Copy Markdown
Member

@Lms24 Lms24 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for porting this instrumentation! I had some questions and suggestions. I got a bit confused how far along we are with "Sentryfying" the instrumentation. I think it's fine if we make certain changes, like simplifying the span name logic, in follow-up PRs if the goal of this PR was primarily to vendor in code. Then again, we definitely made some changes in there already, so I suggested them anyway. Therefore, feel free to address some of my comments in a follow-up PR.

'express.type': 'request_handler',
'http.route': '/test-transaction',
'sentry.origin': 'auto.http.otel.express',
'sentry.origin': 'auto.http.express',
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a comment on the origin change (not just this specific line): I looked up (with Hex) if any alerts, dashboards or saved explore queries depend on sentry.origin (or span.origin which is what the product maps the attribute to for better or worse):

  • Looks like no alerts are configured ✅
  • only 10 dashboard widgets depend on any sentry.origin attributes. All of these are set on the same dashboard for the same org and none depend directly on auto.http.otel.express
  • No discover queries (though Discover is a legacy feature anyway) ✅
  • I didn't yet find any data for saved explore queries, so we're flying blind here for the moment. My guess is usage will be very low/non-existing.

Which means that I think it's fine to change the value now. No need to wait for a new major. Will try to get some more information on saved explore queries but this shouldn't block us.

@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from 37d4883 to a564a72 Compare March 25, 2026 23:48
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch 2 times, most recently from ff8586c to c78e960 Compare March 26, 2026 15:50
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from c78e960 to 3c1ebe0 Compare March 26, 2026 23:38
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from 2d55c15 to 4adacd5 Compare March 27, 2026 02:54
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from fe09c38 to f87db82 Compare March 27, 2026 17:12
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from f87db82 to ad38395 Compare March 27, 2026 17:51
}

// verify against the config if the layer should be ignored
if (isLayerIgnored(metadata.name, type, options)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

isLayerIgnored receives prefixed name, breaking ignoreLayers matching

High Severity

isLayerIgnored is called with metadata.name (e.g., 'middleware - query') instead of the simple layer name (e.g., 'query'). Since stringMatchesSomePattern is invoked with requireExactStringMatch: true, user-provided string patterns in ignoreLayers like ['query'] will never match, effectively breaking the ignoreLayers configuration option. The correct value to pass is metadata.attributes[ATTR_EXPRESS_NAME] (the undecorated layer name), matching the original OTel instrumentation behavior.

Additional Locations (1)
Fix in Cursor Fix in Web

@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from ad38395 to 15a772d Compare March 27, 2026 23:03
Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

This extracts the functionality from the OTel Express intstrumentation,
replacing it with a portable standalone integration in `@sentry/core`,
which can be extended and applied to patch any Express module import in
whatever way makes sense for the platform in question.

Currently in node, that is still an OpenTelemetry intstrumentation, but
just handling the automatic module load instrumentation, not the entire
tracing integration.

This is somewhat a proof of concept, to see what it takes to port a
fairly invovled OTel integration into a state where it can support all
of the platforms that we care about, but it does impose a bit less of a
translation layer between OTel and Sentry semantics (for example, no
need to use the no-op `span.recordException()`).

User-visible changes (beyond the added export in `@sentry/core`):

- Express router spans have an origin of `auto.http.express` rather than
  `auto.http.otel.express`, since it's no longer technically an otel
  integration.
- The empty `measurements: {}` object is no longer attached to span
  data, as that was an artifact of otel's `span.recordError`, which is a
  no-op anyway, and no longer executed.

Obviously this is not a full clean-room reimplementation, and relies on
the fact that the opentelemetry-js-contrib project is Apache 2.0
licensed. I included the link to the upstream license in the index file
for the Express integration, but there may be a more appropriate way to
ensure that the license is respected properly. It was arguably a
derivative work already, but simple redistribution is somewhat different
than re-implementation with subtly different context.

This reduces the node overhead and makes the Express instrumentation
portable to other SDKs, but it of course *increases* the bundle size of
`@sentry/core` considerably. It would be a good idea to explore
splitting out integrations from core, so that they're bundled and
analyzed separately, rather than shipping to all SDKs that extend core.
@isaacs isaacs force-pushed the isaacschlueter/portable-express-integration branch from 15a772d to 4c52729 Compare March 27, 2026 23:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(core, node): portable Express integration

2 participants